package org.mkh.cmd;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;


/**
 The Class AbstractCommand. Startpoint for clients. Extend AbstractCommand and provide an implementation of the
execute() method.
 In the subclass provide any required    FieldInjectionMarkers to identify the source and names of fields
 used in the implementation for example
 <code>
  FieldInjectionMarker(name = "width", sourceClass="org.mkh.cmd.DataModel", returnValue = true)
 	private int width;

	FieldInjectionMarker(name = "height", sourceClass="org.mkh.cmd.DataModel", returnValue = false)
	private int height;

	FieldInjectionMarker(name = "area", sourceClass="org.mkh.cmd.DataModel", returnValue = false)
	private int area;
 </code>
*/
public abstract class AbstractCommand implements CommandIF, InvocationHandler {
// ------------------------ INTERFACE METHODS ------------------------


// --------------------- Interface InvocationHandler ---------------------

    public final Object invoke(final Object proxy,final Method method, final Object[] args) throws Throwable {
        invokeCommand();
        return null;
    }

// -------------------------- OTHER METHODS --------------------------

    /**
     * This is the entry point for the command. invocation order is
     * <p/>
     * 1. invokes the pre callback method based on any @PreCommandCallbackMarker
     * annotation within this (sub)class.
     * <p/>
     * 2. gets the values of any parameters
     * as defined by any @FieldInjectionMarker annotations within this
     * (sub)class.
     *  3. invokes the execute() method. 4. returns the values of any
     * parameters as defined by any @FieldInjectionMarker annotations within
     * this (sub)class. 5. invokes the post callback method based on any @PostCommandCallbackMarker
     * annotation within this (sub)class.
     *
     */
    private  void invokeCommand() {
        try {
            invokePreCallback();
            getParams();
            execute();
            returnParams();
            invokePostCallback();
        } catch (final SecurityException e) {
            e.printStackTrace();
        } catch (final IllegalArgumentException e) {
            e.printStackTrace();
        } catch (final ClassNotFoundException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } catch (final NoSuchFieldException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } catch (final IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } catch (final NoSuchMethodException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } catch (final InvocationTargetException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * Invoke pre callback.
     *
     * @throws SecurityException         the security exception
     * @throws NoSuchMethodException     the no such method exception
     * @throws IllegalArgumentException  the illegal argument exception
     * @throws IllegalAccessException    the illegal access exception
     * @throws InvocationTargetException the invocation target exception
     * @throws ClassNotFoundException    the class not found exception
     */
    private void invokePreCallback() throws SecurityException, NoSuchMethodException, IllegalArgumentException,
            IllegalAccessException, InvocationTargetException, ClassNotFoundException {
        final Method method = this.getClass().getMethod("execute", null);
        if (!method.isAnnotationPresent(PreCommandCallbackMarker.class))
            return;

        // otherwise do the callback
        // check the method exists
        final PreCommandCallbackMarker marker = method.getAnnotation(PreCommandCallbackMarker.class);
        final String targetClassName = marker.targetClass();
        final String methodName = marker.methodName();
        final Class targetClass = this.getClass().getClassLoader().loadClass(targetClassName);
        final Method targetMethod = targetClass.getMethod(methodName, null);

        // get the instance of the class from the registry
        final Object targetObject = Registry.get(targetClassName);
        if (targetObject == null) {
            throw new RuntimeException(targetClassName + " is not known in the Registry");
        }
        // and call it
        targetMethod.invoke(targetObject);
    }

    /**
     * Gets the params.
     *
     * @return the params
     * @throws ClassNotFoundException   the class not found exception
     * @throws SecurityException        the security exception
     * @throws NoSuchFieldException     the no such field exception
     * @throws IllegalArgumentException the illegal argument exception
     * @throws IllegalAccessException   the illegal access exception
     */
    private void getParams() throws ClassNotFoundException, SecurityException, NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        // look in this class for FieldInjectionMarker
        for (final Field field : this.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(FieldInjectionMarker.class)) {
                // found a FieldInjectionMarker, so find which class need to
                // refer to
                final FieldInjectionMarker fm = field.getAnnotation(FieldInjectionMarker.class);
                final String clazzName = fm.sourceClass();

                final String fieldName = fm.name();

                // look in the class for thr field
                final Class sourceClass = this.getClass().getClassLoader().loadClass(clazzName);
                final Field sourceField = sourceClass.getDeclaredField(fieldName);
                sourceField.setAccessible(true);

                final Object sourceObject = Registry.get(clazzName);
                final Object newVal = sourceField.get(sourceObject);
                sourceField.setAccessible(false);
                // temporarily change accessibility
                field.setAccessible(true);
                field.set(this, newVal);
                field.setAccessible(false);
            }
        }
    }

 /**
     * This is the entry point for the command. The subclass provides and implementation and internally the invocation order is
     * <p/>
     * 1. invokes the pre callback method based on any @PreCommandCallbackMarker
     * annotation within this (sub)class.
     * <p/>
     * 2. gets the values of any parameters
     * as defined by any @FieldInjectionMarker annotations within this
     * (sub)class.
     *  3. invokes the execute() method. 4. returns the values of any
     * parameters as defined by any @FieldInjectionMarker annotations within
     * this (sub)class. 5. invokes the post callback method based on any @PostCommandCallbackMarker
     * annotation within this (sub)class.
     *
     */
    public abstract void execute();

    /**
     * Return params.
     *
     * @throws ClassNotFoundException   the class not found exception
     * @throws SecurityException        the security exception
     * @throws NoSuchFieldException     the no such field exception
     * @throws IllegalArgumentException the illegal argument exception
     * @throws IllegalAccessException   the illegal access exception
     */
    private void returnParams() throws ClassNotFoundException, SecurityException, NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        // look in this class for FieldInjectionMarker
        for (final Field field : this.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(FieldInjectionMarker.class)) {
                // found a FieldInjectionMarker, so find which class need to
                // refer to
                final FieldInjectionMarker fm = field.getAnnotation(FieldInjectionMarker.class);

                // it is annotated but is it marked to return the value back to
                // the mediator?
                final boolean returnInjection = fm.returnValue();
                if (returnInjection) {
                    final String clazzName = fm.sourceClass();
                    final String fieldName = fm.name();

                    // look in THIS class for thr field
                    final Class sourceClass = this.getClass();
                    final Field sourceField = sourceClass.getDeclaredField(fieldName);
                    sourceField.setAccessible(true);

                    // get the value
                    final Object sourceObject = this;
                    final Object newVal = sourceField.get(sourceObject);
                    sourceField.setAccessible(false);

                    // dest class
                    final Class destClass = this.getClass().getClassLoader().loadClass(clazzName);
                    final Field destField = destClass.getDeclaredField(fieldName);
                    final Object destObject = Registry.get(clazzName);
                    // temporarily change accessibility and set the value
                    destField.setAccessible(true);
                    destField.set(destObject, newVal);
                    destField.setAccessible(false);
                }
            }
        }
    }

    /**
     * Invoke post callback.
     *
     * @throws SecurityException         the security exception
     * @throws NoSuchMethodException     the no such method exception
     * @throws IllegalArgumentException  the illegal argument exception
     * @throws IllegalAccessException    the illegal access exception
     * @throws InvocationTargetException the invocation target exception
     * @throws ClassNotFoundException    the class not found exception
     */
    private void invokePostCallback() throws SecurityException, NoSuchMethodException, IllegalArgumentException,
            IllegalAccessException, InvocationTargetException, ClassNotFoundException {
        final Method method = this.getClass().getMethod("execute", null);
        if (!method.isAnnotationPresent(PostCommandCallbackMarker.class))
            return;

        // otherwise do the callback
        // check the method exists
        final PostCommandCallbackMarker marker = method.getAnnotation(PostCommandCallbackMarker.class);
        final String targetClassName = marker.targetClass();
        final String methodName = marker.methodName();
        final Class targetClass = this.getClass().getClassLoader().loadClass(targetClassName);
        final Method targetMethod = targetClass.getMethod(methodName, null);

        // get the instance of the class from the registry
        final Object targetObject = Registry.get(targetClassName);
        if (targetObject == null) {
            throw new RuntimeException(targetClassName + " is not known in the Registry");
        }
        // and call it
        targetMethod.invoke(targetObject);
    }
}
